import React, { useState, useEffect, useCallback, useRef } from "react";
import { ListProps, AutoSizer } from "react-virtualized";
import ResizeObserver from "resize-observer-polyfill";
import { isEqual, omit, debounce } from "lodash";

/**
 * 当虚拟列表的滚动条不在自身时使用
 * <AutoHeightListContainer {...}>
 *   {(props) => <List {...props} rowRenderer={...} />}
 * </AutoHeightListContainer>.
 *
 * Q & A:
 * - 为什么不用 WindowScroller？
 *   WindowScroller 在 scrollElement 尺寸变化时能够重新计算当前节点距离顶部的距离，而这在
 *   结合 popup 使用时，如果关闭后再打开，会出现不正确的行为：
 *     WindowScroller._positionFormTop === scrollElement.scrollTop
 *
 * - 为什么不使用 AutoSizer？
 *   可通过 AutoSizer 动态得到当前的宽度值，然而，因为组件内部需要维护高度值来适应以下情况：
 *     1. 滚动元素高度变化
 *   此时可以同时维护一个宽度值，所以就自维护了
 */
function AutoHeightListContainer<T extends HTMLElement>(props: {
   /**
   * 行高
   */
  rowHeight: number;
  /**
   * 行数
   */
  rowCount: number;
  /**
   * 渲染列表
   */
  children: (
    necessaryProps: Pick<
      ListProps,
      | "rowCount"
      | "rowHeight"
      | "autoHeight"
      | "height"
      | "isScrolling"
      | "scrollTop"
      | "onScroll"
      | "width"
      | "overscanRowCount"
    >
  ) => React.ReactNode;
  /**
   * 可滚动的父系元素，不填默认为父元素
   */
  scrollElement?: React.RefObject<T | null> | HTMLElement;
    /**
   * 无可滚动的父元素时显示的内容
   */
  unrenderNode?: React.ReactNode;
  /**
   * 预渲染元素数量
   */
  overscanRowCount?: number;
  /**
   * 状态由 isScrolling -> !isScrolling 的间隔时间
   */
  isScrollingTimeout?: number;
  /**
   * 元素尺寸变化导致状态变更的触发间隔，-1 表示不使用 debounce
   */
  updateStateDebounce?: number;
}) {
  const {
    rowCount,
    rowHeight,
    children: render,
    scrollElement,
    overscanRowCount = AutoHeightListContainer.defaultProps.overscanRowCount,
    unrenderNode,
    isScrollingTimeout,
    updateStateDebounce,
  } = props;

  // 属性状态
  const [state, setState] = useState<{
    container: T | HTMLElement | null;
    width: number;
    height: number;
    offsetTop: number;
    scrollTop: number;
    isScrolling: boolean;
  }>({
    width: 0,
    height: 0,
    container: null,
    offsetTop: 0,
    scrollTop: 0,
    isScrolling: false,
  });

  const { width, height, container, offsetTop, scrollTop, isScrolling } = state;

  // 容器元素属性
  const nodeRef = useRef<HTMLDivElement | null>(null);
  const totalHeight = rowCount * rowHeight;

  useEffect(() => {
    const currentNodeRef = nodeRef.current;

    if (currentNodeRef == null) {
      return;
    }

    let newContainer: typeof container = null;
    if (scrollElement === undefined) {
      newContainer = currentNodeRef.parentElement || null;
    } else {
      newContainer = isRef<T | null>(scrollElement)
        ? scrollElement.current
        : scrollElement;
    }
    if (newContainer == null) {
      setState(() => ({
        container: null,
        width: 0,
        height: 0,
        offsetTop: 0,
        scrollTop: 0,
        isScrolling: false,
      }));
      return;
    }

    let width = currentNodeRef.getBoundingClientRect().width,
      height = newContainer.getBoundingClientRect().height;
    // 设置 relative 以保证 offsetTop正确
    let positionStyle = window.getComputedStyle(newContainer).position;
    if (positionStyle === "static") {
      newContainer.style.position = "relative";
    }

    const updateState = function updateState() {
      const offsetTop = currentNodeRef.offsetTop;
      if (
        currentNodeRef.offsetParent != null &&
        currentNodeRef.offsetParent !== newContainer
      ) {
        console.warn(
          "offsetParent 不为当前滚动容器，计算可能有偏差",
          currentNodeRef.offsetParent,
          newContainer
        );
      }
      setState((state) => {
        if (newContainer == null) {
          return state;
        }
        const newState = {
          width,
          height,
          container: newContainer,
          offsetTop,
          scrollTop: newContainer.scrollTop - offsetTop,
          isScrolling: false,
        };
        if (
          isEqual(omit(newState, "container"), omit(state, "container")) &&
          container === newContainer
        ) {
          return state;
        }
        return newState;
      });
    };
    updateState();

    // resize detector
    const updateStateD = debounce(updateState, updateStateDebounce);
    const roc = new ResizeObserver((entries) => {
      let modified = false;
      entries.forEach((entry) => {
        if (entry == null) {
          return;
        }
        if (entry.target === container && height !== entry.contentRect.height) {
          height = entry.contentRect.height;
          modified = true;
        }
        if (
          entry.target === currentNodeRef &&
          width !== entry.contentRect.width
        ) {
          width = entry.contentRect.width;
          modified = true;
        }
      });
      if (!modified) {
        return;
      }
      if (updateStateDebounce === -1) {
        updateState();
      } else {
        updateStateD();
      }
    });
    roc.observe(newContainer);
    roc.observe(currentNodeRef);
    return () => {
      roc.disconnect();
      updateStateD.cancel();
    };
  }, [container, scrollElement, updateStateDebounce]);

  useEffect(() => {
    const currentNodeRef = nodeRef.current;
    if (container == null || currentNodeRef == null) {
      return;
    }
    let timer: number | null = null;
    const updateStateForScroll = function updateStateForScroll() {
      timer != null && window.clearTimeout(timer);
      timer = window.setTimeout(() => {
        setState((state) => ({
          ...state,
          isScrolling: false,
        }));
      }, isScrollingTimeout);

      setState((state) => {
        const offsetTop = currentNodeRef.offsetTop;
        return {
          ...state,
          offsetTop: offsetTop,
          scrollTop: container.scrollTop - offsetTop,
          isScrolling: true,
        };
      });
    };
    container.addEventListener("scroll", updateStateForScroll);
    return () => {
      container.removeEventListener("scroll", updateStateForScroll);
      timer !== null && window.clearTimeout(timer);
    };
  }, [container, isScrollingTimeout]);

  const handleListScroll = useCallback(
    ({ scrollTop: nextScrollTop }: { scrollTop: number }) => {
      const resultedScrollTop = nextScrollTop;
      if (resultedScrollTop === Math.max(0, scrollTop)) {
        return;
      }
      container?.scrollTo({
        top: offsetTop + nextScrollTop,
      });
    },
    [container, offsetTop, scrollTop]
  );

  return (
    <div
      style={{
        height: totalHeight,
        position: "relative",
      }}
      ref={nodeRef}
    >
      {container == null
        ? unrenderNode
        : render({
            autoHeight: true,
            /**
             * 对于不在视区的元素高度设为 0，阻止渲染子元素
             */
            height:
              scrollTop < (-overscanRowCount * rowHeight - height) ||
              scrollTop > overscanRowCount * rowHeight + totalHeight
                ? 0
                : height,
            isScrolling: isScrolling,
            scrollTop: Math.max(0, scrollTop),
            onScroll: handleListScroll,
            rowHeight: rowHeight,
            rowCount: rowCount,
            overscanRowCount,
            width,
          })}
    </div>
  );
}

AutoHeightListContainer.defaultProps = {
  AutoHeightListContainer: "...",
  isScrollingTimeout: 150,
  updateStateDebounce: 16,
  overscanRowCount: 5,
};

export default AutoHeightListContainer;

function isRef<T>(ele: any): ele is React.RefObject<T> {
  return "current" in ele;
}
